package kratoslog

import (
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

const (
	LoggerKey  = "zapLogger"
	TimeFormat = "2006-01-02 15:04:05.000000000"
)

var (
	_levelToColor = map[zapcore.Level]Color{
		zapcore.DebugLevel:  Magenta,
		zapcore.InfoLevel:   Blue,
		zapcore.WarnLevel:   Yellow,
		zapcore.ErrorLevel:  Red,
		zapcore.DPanicLevel: Red,
		zapcore.PanicLevel:  Red,
		zapcore.FatalLevel:  Red,
	}
	_unknownLevelColor = Red

	_levelToLowercaseColorString = make(map[zapcore.Level]string, len(_levelToColor))
	_levelToCapitalColorString   = make(map[zapcore.Level]string, len(_levelToColor))
)

type Log struct {
	Console    bool
	Compress   bool
	RunMode    string
	Level      string
	Dir        string
	Encoding   string
	FileName   string
	MaxAge     int
	MaxSize    int
	MaxBackups int
}

func NewLog(conf *viper.Viper) *Log {
	encoding := strings.TrimSpace(conf.GetString("log.encoding"))
	level := strings.TrimSpace(conf.GetString("log.level"))
	dir := strings.TrimSpace(conf.GetString("log.dir"))
	fileName := strings.TrimSpace(conf.GetString("log.file_name"))
	maxAge := conf.GetUint("log.max_age")
	maxSize := conf.GetUint("log.max_size")
	maxBackups := conf.GetUint("log.max_backups")
	compress := conf.GetBool("log.compress")
	console := conf.GetBool("log.console")

	if encoding == "" {
		encoding = "text"
	}
	if level == "" {
		level = "info"
	}
	if maxSize == 0 {
		maxSize = 200
	}
	if maxAge == 0 {
		maxAge = 30
	}
	if maxBackups == 0 {
		maxBackups = 50
	}

	return &Log{
		Console:    console,
		Compress:   compress,
		Level:      level,
		Dir:        dir,
		Encoding:   encoding,
		FileName:   fileName,
		MaxAge:     int(maxAge),
		MaxSize:    int(maxSize),
		MaxBackups: int(maxBackups),
	}
}

type Logger struct {
	*zap.Logger
}

func NewLogger(log *Log) *Logger {
	return &Logger{NewZapLogger(log)}
}

func NewZapLogger(log *Log) *zap.Logger {
	if log.FileName == "" {
		log.FileName = "running.log"
	}

	file := filepath.Join(log.Dir, log.FileName)
	var level zapcore.Level
	switch log.Level {
	case "debug":
		level = zap.DebugLevel
	case "info":
		level = zap.InfoLevel
	case "warn":
		level = zap.WarnLevel
	case "error":
		level = zap.ErrorLevel
	default:
		level = zap.InfoLevel
	}
	hook := lumberjack.Logger{
		Filename:   file,           // Log file path
		MaxSize:    log.MaxSize,    // Maximum size unit for each log file: M
		MaxBackups: log.MaxBackups, // The maximum number of backups that can be saved for log files
		MaxAge:     log.MaxAge,     // Maximum number of days the file can be saved
		Compress:   log.Compress,   // Compression or not
	}

	var encoder zapcore.Encoder
	switch log.Encoding {
	case "console":
		encoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
			TimeKey:        "ts",
			LevelKey:       "level",
			NameKey:        "logger",
			CallerKey:      "caller",
			MessageKey:     "msg",
			StacktraceKey:  "stacktrace",
			LineEnding:     zapcore.DefaultLineEnding,
			EncodeLevel:    zapcore.CapitalColorLevelEncoder,
			EncodeTime:     timeEncoder,
			EncodeDuration: zapcore.SecondsDurationEncoder,
			EncodeCaller:   zapcore.ShortCallerEncoder,
		})
	case "json":
		encoder = zapcore.NewJSONEncoder(zapcore.EncoderConfig{
			TimeKey:        "ts",
			LevelKey:       "level",
			NameKey:        "logger",
			CallerKey:      "caller",
			MessageKey:     "msg",
			StacktraceKey:  "stacktrace",
			LineEnding:     zapcore.DefaultLineEnding,
			EncodeLevel:    zapcore.CapitalLevelEncoder,
			EncodeTime:     zapcore.EpochNanosTimeEncoder,
			EncodeDuration: zapcore.SecondsDurationEncoder,
			EncodeCaller:   zapcore.ShortCallerEncoder,
		})
	default:
		encoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
			TimeKey:        "ts",
			LevelKey:       "level",
			NameKey:        "logger",
			CallerKey:      "caller",
			MessageKey:     "msg",
			StacktraceKey:  "stacktrace",
			LineEnding:     zapcore.DefaultLineEnding,
			EncodeLevel:    levelEncoder,
			EncodeTime:     timeEncoder,
			EncodeDuration: zapcore.SecondsDurationEncoder,
			EncodeCaller:   callerEncoder,
		})
	}

	writer := []zapcore.WriteSyncer{
		zapcore.AddSync(&hook),
	}
	if log.Console {
		writer = append(writer, zapcore.AddSync(os.Stdout))
	}

	core := zapcore.NewCore(
		encoder,
		zapcore.NewMultiWriteSyncer(writer...),
		level,
	)

	switch log.RunMode {
	case "prod":
		return zap.New(core, zap.Development(), zap.AddCaller())
	case "kratos":
		return zap.New(core)
	default:
		return zap.New(core, zap.AddCaller())
	}
}

func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
	enc.AppendString("[" + t.Format(TimeFormat) + "]")
}

func levelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
	s, ok := _levelToCapitalColorString[level]
	if !ok {
		s = _unknownLevelColor.Add(level.CapitalString())
	}
	enc.AppendString("[" + s + "]")
}

func callerEncoder(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
	enc.AppendString("[" + caller.TrimmedPath() + "]")
}

// NewContext Adds a field to the specified context
func (l *Logger) NewContext(ctx *gin.Context, fields ...zapcore.Field) {
	ctx.Set(LoggerKey, l.WithContext(ctx).With(fields...))
}

// WithContext Returns a zap instance from the specified context
func (l *Logger) WithContext(ctx *gin.Context) *Logger {
	if ctx == nil {
		return l
	}
	zl, _ := ctx.Get(LoggerKey)
	ctxLogger, ok := zl.(*zap.Logger)
	if ok {
		return &Logger{ctxLogger}
	}
	return l
}

func init() {
	for level, color := range _levelToColor {
		_levelToLowercaseColorString[level] = color.Add(level.String())
		_levelToCapitalColorString[level] = color.Add(level.CapitalString())
	}
}
